/*
 * Copyright 2011 Software Freedom Conservancy.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package com.thoughtworks.selenium;

import com.google.common.base.Charsets;
import com.google.common.collect.Lists;

import org.openqa.selenium.net.Urls;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Arrays;
import java.util.List;

/**
 * Sends commands and retrieves results via HTTP.
 * 
 * @author Ben Griffiths, Jez Humble
 */
public class HttpCommandProcessor implements CommandProcessor {

  private String pathToServlet;
  private String browserStartCommand;
  private String browserURL;
  private String sessionId;
  private String extensionJs;
  private String rcServerLocation;

  /**
   * Specifies a server host/port, a command to launch the browser, and a starting URL for the
   * browser.
   * 
   * @param serverHost - the host name on which the Selenium Server resides
   * @param serverPort - the port on which the Selenium Server is listening
   * @param browserStartCommand - the command string used to launch the browser, e.g. "*firefox" or
   *        "c:\\program files\\internet explorer\\iexplore.exe"
   * @param browserURL - the starting URL including just a domain name. We'll start the browser
   *        pointing at the Selenium resources on this URL,
   * @param extensionJs - extension Javascript for this session e.g. "http://www.google.com" would
   *        send the browser to "http://www.google.com/selenium-server/core/RemoteRunner.html"
   */
  public HttpCommandProcessor(String serverHost, int serverPort, String browserStartCommand,
      String browserURL) {
    rcServerLocation = serverHost +
        ":" + Integer.toString(serverPort);
    this.pathToServlet = "http://" + rcServerLocation + "/selenium-server/driver/";
    this.browserStartCommand = browserStartCommand;
    this.browserURL = browserURL;
    this.extensionJs = "";
  }

  /**
   * Specifies the URL to the CommandBridge servlet, a command to launch the browser, and a starting
   * URL for the browser.
   * 
   * @param pathToServlet - the URL of the Selenium Server Driver, e.g.
   *        "http://localhost:4444/selenium-server/driver/" (don't forget the final slash!)
   * @param browserStartCommand - the command string used to launch the browser, e.g. "*firefox" or
   *        "c:\\program files\\internet explorer\\iexplore.exe"
   * @param browserURL - the starting URL including just a domain name. We'll start the browser
   *        pointing at the Selenium resources on this URL,
   * @param extensionJs - extension Javascript for this session
   */
  public HttpCommandProcessor(String pathToServlet, String browserStartCommand, String browserURL) {
    this.pathToServlet = pathToServlet;
    this.browserStartCommand = browserStartCommand;
    this.browserURL = browserURL;
    this.extensionJs = "";
  }

  public String getRemoteControlServerLocation() {
    return rcServerLocation;
  }

  public String doCommand(String commandName, String[] args) {
    DefaultRemoteCommand command = new DefaultRemoteCommand(commandName, args);
    String result = executeCommandOnServlet(command.getCommandURLString());
    if (result == null) {
      throw new NullPointerException("Selenium Bug! result must not be null");
    }
    if (!result.startsWith("OK")) {
      return throwAssertionFailureExceptionOrError(result);
    }
    return result;
  }

  protected String throwAssertionFailureExceptionOrError(String message) {
    throw new SeleniumException(message);
  }

  /** Sends the specified command string to the bridge servlet */
  public String executeCommandOnServlet(String command) {
    try {
      return getCommandResponseAsString(command);
    } catch (IOException e) {
      if (e instanceof ConnectException) {
        throw new SeleniumException(e.getMessage(), e);
      }
      e.printStackTrace();
      throw new UnsupportedOperationException("Catch body broken: IOException from " + command +
          " -> " + e, e);
    }
  }

  private String stringContentsOfInputStream(Reader rdr) throws IOException {
    StringBuffer sb = new StringBuffer();
    int c;
    try {
      while ((c = rdr.read()) != -1) {
        sb.append((char) c);
      }
      return sb.toString();
    } finally {
      rdr.close();
    }
  }

  // for testing
  protected HttpURLConnection getHttpUrlConnection(URL urlForServlet) throws IOException {
    return (HttpURLConnection) urlForServlet.openConnection();
  }

  // for testing
  protected Writer getOutputStreamWriter(HttpURLConnection conn) throws IOException {
    return new BufferedWriter(new OutputStreamWriter(conn.getOutputStream(), Charsets.UTF_8));
  }

  // for testing
  protected Reader getInputStreamReader(HttpURLConnection conn) throws IOException {
    return new InputStreamReader(conn.getInputStream(), "UTF-8");
  }

  // for testing
  protected int getResponseCode(HttpURLConnection conn) throws IOException {
    return conn.getResponseCode();
  }

  protected String getCommandResponseAsString(String command) throws IOException {
    String responseString = null;
    int responsecode = HttpURLConnection.HTTP_MOVED_PERM;
    HttpURLConnection uc = null;
    Writer wr = null;
    Reader rdr = null;
    while (responsecode == HttpURLConnection.HTTP_MOVED_PERM) {
      URL result = new URL(pathToServlet);
      String body = buildCommandBody(command);
      try {
        uc = getHttpUrlConnection(result);
        uc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
        uc.setInstanceFollowRedirects(false);
        uc.setDoOutput(true);
        wr = getOutputStreamWriter(uc);
        wr.write(body);
        wr.flush();
        responsecode = getResponseCode(uc);
        if (responsecode == HttpURLConnection.HTTP_MOVED_PERM) {
          pathToServlet = uc.getRequestProperty("Location");
        } else if (responsecode != HttpURLConnection.HTTP_OK) {
          throwAssertionFailureExceptionOrError(uc.getResponseMessage());
        } else {
          rdr = getInputStreamReader(uc);
          responseString = stringContentsOfInputStream(rdr);
        }
      } finally {
        closeResources(uc, wr, rdr);
      }
    }
    return responseString;
  }

  protected void closeResources(HttpURLConnection conn, Writer wr, Reader rdr) {
    try {
      if (null != wr) {
        wr.close();
      }
    } catch (IOException ioe) {
      // ignore
    }

    try {
      if (null != rdr) {
        rdr.close();
      }
    } catch (IOException ioe) {
      // ignore
    }

    if (null != conn) {
      conn.disconnect();
    }
  }

  private String buildCommandBody(String command) {
    StringBuffer sb = new StringBuffer();
    sb.append(command);
    if (sessionId != null) {
      sb.append("&sessionId=");
      sb.append(Urls.urlEncode(sessionId));
    }
    return sb.toString();
  }

  /**
   * This should be invoked before start().
   * 
   * @param extensionJs the extra extension Javascript to include in this browser session.
   */
  public void setExtensionJs(String extensionJs) {
    this.extensionJs = extensionJs;
  }

  public void start() {
    String result = getString("getNewBrowserSession",
        new String[] {browserStartCommand, browserURL, extensionJs});
    setSessionInProgress(result);
  }

  public void start(String optionsString) {
    String result = getString("getNewBrowserSession",
        new String[] {browserStartCommand, browserURL,
            extensionJs, optionsString});
    setSessionInProgress(result);
  }

  /**
   * Wraps the version of start() that takes a String parameter, sending it the result of calling
   * toString() on optionsObject, which will likely be a BrowserConfigurationOptions instance.
   * 
   * @param optionsObject
   */
  public void start(Object optionsObject) {
    start(optionsObject.toString());
  }

  protected void setSessionInProgress(String result) {
    sessionId = result;
  }

  public void stop() {
    if (hasSessionInProgress()) {
      doCommand("testComplete", null);
    }
    setSessionInProgress(null);
  }

  public boolean hasSessionInProgress() {
    return null != sessionId;
  }

  public String getString(String commandName, String[] args) {
    String result = doCommand(commandName, args);
    if (result.length() >= "OK,".length()) {
      return result.substring("OK,".length());
    }
    System.err.println("WARNING: getString(" + commandName + ") saw a bad result " + result);
    return "";
  }

  public String[] getStringArray(String commandName, String[] args) {
    String result = getString(commandName, args);
    return parseCSV(result);
  }

  /**
   * Convert backslash-escaped comma-delimited string into String array. As described in SRC-CDP
   * spec section 5.2.1.2, these strings are comma-delimited, but commas can be escaped with a
   * backslash "\". Backslashes can also be escaped as a double-backslash.
   * 
   * @param input the unparsed string, e.g. "veni\, vidi\, vici,c:\\foo\\bar,c:\\I came\, I
   *        \\saw\\\, I conquered"
   * @return the string array resulting from parsing this string
   */
  public static String[] parseCSV(String input) {
    List<String> output = Lists.newArrayList();
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < input.length(); i++) {
      char c = input.charAt(i);
      switch (c) {
        case ',':
          output.add(sb.toString());
          sb = new StringBuffer();
          continue;
        case '\\':
          i++;
          c = input.charAt(i);
          // fall through to:
        default:
          sb.append(c);
      }
    }
    output.add(sb.toString());
    return output.toArray(new String[output.size()]);
  }

  public Number getNumber(String commandName, String[] args) {
    String result = getString(commandName, args);
    Number n;
    try {
      n = NumberFormat.getInstance().parse(result);
    } catch (ParseException e) {
      throw new RuntimeException(e);
    }
    if (n instanceof Long && n.intValue() == n.longValue()) {
      // SRC-315 we should return Integers if possible
      return Integer.valueOf(n.intValue());
    }
    return n;
  }

  public Number[] getNumberArray(String commandName, String[] args) {
    String[] result = getStringArray(commandName, args);
    Number[] n = new Number[result.length];
    for (int i = 0; i < result.length; i++) {
      try {
        n[i] = NumberFormat.getInstance().parse(result[i]);
      } catch (ParseException e) {
        throw new RuntimeException(e);
      }
    }
    return n;
  }

  public boolean getBoolean(String commandName, String[] args) {
    String result = getString(commandName, args);
    boolean b;
    if ("true".equals(result)) {
      b = true;
      return b;
    }
    if ("false".equals(result)) {
      b = false;
      return b;
    }
    throw new RuntimeException("result was neither 'true' nor 'false': " + result);
  }

  public boolean[] getBooleanArray(String commandName, String[] args) {
    String[] result = getStringArray(commandName, args);
    boolean[] b = new boolean[result.length];
    for (int i = 0; i < result.length; i++) {
      if ("true".equals(result[i])) {
        b[i] = true;
        continue;
      }
      if ("false".equals(result[i])) {
        b[i] = false;
        continue;
      }
      throw new RuntimeException("result was neither 'true' nor 'false': " +
          Arrays.toString(result));
    }
    return b;
  }

}
